# -*- coding: utf-8 -*-
"""
作者：https://gitee.com/acc8226
项目：https://gitee.com/acc8226/easy-upload 轻量级文件上传服务-适用于琐碎文件收集
声明：本代码为原创作品，遵循 木兰宽松许可证（第2版）许可证。
"""

from config import *
from flask import Flask, request, jsonify, redirect, url_for, session, send_from_directory, \
    send_file, abort, after_this_request, render_template
import os
import tempfile
import uuid
import yaml
import subprocess
import qrcode
from datetime import datetime
from io import BytesIO
from pathlib import Path

# 从 Python 3.10 起，官方文档已把 typing.List 标记为「遗留别名」，鼓励使用内置泛型。
from typing import List

from ipfilter import IPFilter

# 读取 YAML
with open(Path(__file__).with_name('user_settings.yml'), encoding='utf-8') as f:
    cfg = yaml.safe_load(f)

# 动态赋值
BROWSE_ENABLED = cfg['browse_enabled']
UPLOAD_PASSWORD = cfg['upload_password']
SUPER_ENABLED = cfg['super_enabled']
SUPER_PASSWORD = cfg['super_password']
ALLOW_DOWNLOAD = cfg.get('allow_download', False)  # 默认为关闭

if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)

app = Flask(__name__)
app.secret_key = os.urandom(24)  # 用于加密 session

ipFilter = IPFilter.from_list(BLOCKED_IPS)


# 处理下载请求
@app.route('/uploads/<path:filename>', methods=['GET'])
def uploaded_file(filename):
    user_ip = get_real_ip()
    # ip 限制
    if ipFilter.is_blocked(user_ip):
        return f"{user_ip} 暂不允许下载文件", 403
    # 只允许访问今天的目录
    today = datetime.now().strftime('%Y-%m-%d')
    if not filename.startswith(today + '/'):
        return "只能下载当天上传的文件", 403
    return send_from_directory(UPLOAD_FOLDER, filename, as_attachment=True)


@app.route('/', methods=['GET'])
def home_page():
    # 如果密码不为空
    if UPLOAD_PASSWORD:
        if 'logged_in' in session:
            # 如果用户已经登录，直接显示上传页面
            return render_template('index.html', title=HTML_TITLE, browse_btn=build_browse_btn())
        # 如果未登录，显示登录页面
        return render_template('login.html')
    else:
        # UPLOAD_PASSWORD 为空，直接显示上传页面
        return render_template('index.html', title=HTML_TITLE, browse_btn=build_browse_btn())


@app.route('/login', methods=['POST'])
def login():
    # 检查是否是登录请求
    password = request.form.get('password')
    if password == UPLOAD_PASSWORD:  # 如果密码为空或匹配，则登录成功
        # 如果密码正确，设置 session 并显示上传页面
        session['logged_in'] = True
        return render_template('index.html', title=HTML_TITLE, browse_btn=build_browse_btn())
    else:
        # 如果密码错误，返回错误提示
        return render_template('pwd-error.html')


def build_browse_btn():
    """根据开关和超级会话返回浏览按钮 HTML，或空字符串"""
    return '''
            <div style="position:absolute; top:10px; right:20px;">
                <a href="./browse" style="text-decoration:none; background:#4CAF50; color:white; padding:8px 12px;
                    border-radius:4px;">
                    浏览文件
                </a>
            </div>
            '''


@app.route('/uploadFiles', methods=['POST'])
def upload_files():
    user_ip = get_real_ip()
    if ipFilter.is_blocked(user_ip):
        return jsonify({'results': [f'上传失败：IP {user_ip} 被禁止上传文件']})

    files = request.files.getlist('file')
    if not files or files[0].filename == '':
        return jsonify({'results': ['上传失败：没有选择文件。']})

    # 调试并打印到终端：客户端声称的大小（字节）
    content_len = int(request.headers.get('Content-Length', 0))
    print('Content-Length', content_len)

    today = datetime.now().strftime('%Y-%m-%d')
    today_folder = os.path.join(UPLOAD_FOLDER, today)
    if not os.path.exists(today_folder):
        os.makedirs(today_folder)
    # 计算总文件大小
    total_size = 0
    for file in files:
        if file:
            file.seek(0, os.SEEK_END)
            total_size += file.tell()
            file.seek(0)

    if total_size > MAX_TOTAL_SIZE:
        return jsonify(
            {'results': [f'上传失败：总上传大小超过 {MAX_TOTAL_SIZE / (1024.0 * 1024)} MB 限制，请减少文件数量或大小']})

    results = []
    for file in files:
        if file:
            # 先检查扩展名（代价小）
            filename = file.filename.lower()
            if any(filename.endswith('.' + ext) for ext in BLACKLIST_EXTENSIONS):
                results.append(f'{file.filename} 上传失败：禁止上传的文件类型')
                continue
            # 再检查文件大小（代价大）
            file.seek(0, os.SEEK_END)
            file_size = file.tell()
            file.seek(0)
            if file_size > MAX_FILE_SIZE:
                results.append(f'{file.filename} 上传失败：文件大小超过 {MAX_FILE_SIZE / (1024.0 * 1024)}MB 限制')
                continue

            device_name = request.form.get('device_name', '').strip()
            if not device_name:
                device_name = user_ip
            try:
                # 先提取文件名部分，这部分不含相对路径。如果文件名中包含路径分隔符，直接使用 os.path.splitext 可能会导致 base 包含路径信息。
                filename = os.path.basename(file.filename)
                # 得到文件名 和 后缀
                base, ext = os.path.splitext(filename)

                # 预先拼接 ip
                pre_file_name = f"{base}{ext}"
                file_path = os.path.join(today_folder, pre_file_name)
                if os.path.exists(file_path):
                    timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
                    new_name = f"{base}_{device_name}({timestamp}){ext}"
                else:
                    new_name = pre_file_name
                file_path = os.path.join(today_folder, new_name)
                file.save(file_path)
                # 返回前端时显示原始文件名
                results.append(f'{file.filename} 上传成功')
            except Exception as e:
                results.append(f'{file.filename} 上传失败: {str(e)}')
    return jsonify({'results': results})


# 文件浏览页
@app.route('/browse', methods=['GET'])
def browse_files():
    # 普通用户受开关控制
    if BROWSE_ENABLED:
        pass
    # 超级用户永远放行
    elif SUPER_ENABLED:
        if session.get('super'):
            pass
        else:
            # 需要登录超级管理员访问
            return redirect(url_for('super_login'))  # 回到上传页，按钮即出现
        # return "对普通用户已禁用文件浏览功能", 403
    else:
        return "文件浏览功能已禁用", 403

    user_ip = get_real_ip()
    # ip 限制
    if ipFilter.is_blocked(user_ip):
        return f"{user_ip} 禁止浏览文件，已超出作用域", 403

    # 普通用户受开关控制
    # elif not BROWSE_ENABLED:
    #     return "对普通用户已禁用文件浏览功能", 403

    date = datetime.now().strftime('%Y-%m-%d')  # 不要每一天，只允许今天
    target_dir = os.path.join(UPLOAD_FOLDER, date)
    files = []
    if os.path.exists(target_dir):
        for f in sorted(os.listdir(target_dir),
                        key=lambda x: (-os.path.getmtime(os.path.join(target_dir, x)), x)):
            path = os.path.join(target_dir, f)
            if os.path.isfile(path):
                size = os.path.getsize(path)
                files.append({
                    'name': f,
                    'size': human_size(size),
                    'date': date,
                    'time': datetime.fromtimestamp(os.path.getmtime(path)).strftime('%H:%M:%S'),
                    'type': file_type(f),
                    'is_text': f.lower().endswith(('.txt', '.md', '.json', '.log', '.py', '.sh', '.java', '.desktop',
                                                   '.js', '.markdown', '.md', '.kt', '.c', '.cpp', '.gitignore', '.bat',
                                                   '.cmd', '.yml', '.yaml', '.csv', '.xml', '.conf')),
                    'is_image': f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', 'ico', '.avi', '.webp')),
                    'is_pdf': f.lower().endswith('.pdf'),
                    'is_office': f.lower().endswith(('.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt'))
                })
    return render_template('browse.html', files=files, date=date, title=HTML_TITLE, allow_download=ALLOW_DOWNLOAD)


def file_type(filename: str) -> str:
    """返回人类可读的文件类型"""
    f = filename.lower()
    if f.endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.avif', '.ico')):
        return '图片'
    elif f.endswith(('.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx')):
        return '文档'
    elif f.endswith(('.txt', '.md', '.log', '.json', '.py', '.sh')):
        return '文本'
    elif f.endswith(('.mp4', '.avi', '.mkv')):
        return '视频'
    elif f.endswith(('.mp3', '.wav', '.flac')):
        return '音频'
    else:
        return '其他'


def human_size(size: int) -> str:
    if size < 1024:
        return f"{size} B"  # 直接显示字节
    elif size < 1024 * 1024:
        # 保留 1 位小数，且不会出现 0 KB
        return f"{size / 1024:.1f} KB".replace(".0 KB", " KB")
    else:
        return f"{size / 1024 / 1024:.1f} MB".replace(".0 MB", " MB")


# pdf 预览
@app.route('/previewPDF/<path:filename>')
def preview_pdf(filename):
    pdf_path = os.path.join(UPLOAD_FOLDER, filename)
    if not os.path.exists(pdf_path):
        abort(404)
    try:
        return send_file(pdf_path, as_attachment=False)
    except Exception as e:
        abort(500, f"无法预览PDF文件：{str(e)}")


# office 预览
@app.route('/previewOffice/<path:filename>')
def preview_office(filename):
    office_path = os.path.join(UPLOAD_FOLDER, filename)
    if not os.path.exists(office_path):
        abort(404)

    # 建一个临时文件，名字唯一
    tmp_dir = tempfile.gettempdir()
    tmp_pdf = os.path.join(tmp_dir, f"{uuid.uuid4().hex}.pdf")

    cmd = [
        'libreoffice', '--headless', '--convert-to', 'pdf',
        '--outdir', tmp_dir, office_path
    ]
    try:
        subprocess.run(cmd, check=True)
    except FileNotFoundError:
        return "服务器未安装 LibreOffice，无法预览 Office 文件", 503
    # LibreOffice 会生成同名 pdf，我们改名到 uuid
    expected = os.path.splitext(os.path.basename(office_path))[0] + '.pdf'
    os.rename(os.path.join(tmp_dir, expected), tmp_pdf)

    @after_this_request
    def cleanup(response):
        try:
            os.remove(tmp_pdf)
        except Exception:
            pass
        return response

    return send_file(tmp_pdf, as_attachment=False)


# 新增暗门路由 /super（只做登录）
@app.route('/super', methods=['GET', 'POST'])
def super_login():
    if not SUPER_ENABLED:  # 新增
        return "超级登录已禁用", 403
    # 若是 POST 请求
    if request.method == 'POST':
        pwd = request.form.get('password')
        if pwd == SUPER_PASSWORD:
            session['super'] = True
            return redirect(url_for('browse_files'))  # 回到上传页，按钮即出现
        else:
            return "口令错误", 403
    # GET 返回极简登录页
    return render_template('admin-login.html')


@app.route('/logout', methods=['GET'])
def logout():
    # 清除 session，用户登出
    session.pop('logged_in', None)
    return redirect(url_for('home_page'))


@app.route('/generate_qr_code/<path:filename>', methods=['GET'])
def generate_qrcode(filename):
    # 创建二维码数据
    qr_data = filename
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=3,
        border=1,
    )
    qr.add_data(qr_data)
    qr.make(fit=True)

    # 生成二维码图像
    img = qr.make_image(fill_color="black", back_color="white")

    # 将图像保存到内存中
    img_io = BytesIO()
    img.save(img_io, 'PNG')
    img_io.seek(0)
    # 返回图像
    return send_file(img_io, mimetype='image/png')


def get_real_ip():
    """先取 X-Forwarded-For，再取 X-Real-IP，最后 fallback 到 remote_addr"""
    if request.headers.getlist("X-Forwarded-For"):
        # 可能多个 IP，用逗号分隔，第一个是客户端
        return request.headers.getlist("X-Forwarded-For")[0].split(',')[0].strip()
    return request.headers.get("X-Real-IP") or request.remote_addr or '未知 IP'


if __name__ == '__main__':
    app.run(host='0.0.0.0')
